<?php

/**
 * Description of PicoORM
 *
 * @author michamaciejewski
 */
class PicoORM {
  /**
   *
   * @var PicoORM
   */
  private static $instance = null;

  /**
   *
   * @var array
   */
  private $connections = array();

  /**
   *
   * @var array
   */
  private $modelConnections = array();

  /**
   *
   * @var array 
   */
  private $statements = array();

  /**
   *
   * @var boolan 
   */
  private $inTransaction = false;

  public function begin() {
    if (!$this->inTransaction) {
      foreach ($this->connections as $connection) {
        $connection->getPdo()->beginTransaction();
      }
      $this->inTransaction = true;
    }
  }
  
  /**
   *
   * @param string $class
   * @param string $property
   * 
   * @return mixed
   */
  
  public static function getStatic($class, $property) {
    $reflection = new ReflectionClass($class);
    return $reflection->getStaticPropertyValue($property);
  }

  public function commit() {
    if ($this->inTransaction) {
      foreach ($this->connections as $connection) {
        $connection->getPdo()->commit();
      }
      $this->inTransaction = false;
    }
  }

  public function rollback() {
    if ($this->inTransaction) {
      foreach ($this->connections as $connection) {
        $connection->getPdo()->rollback();
      }
      $this->inTransaction = false;
    }
  }

  /**
   *
   * @return PicoORM
   */
  public static function instance() {
    if (!self::$instance) {
      self::$instance = new PicoORM();
    }

    return self::$instance;
  }

  /**
   *
   * @param string $alias
   * @return Connection
   */
  public function getConnection($alias) {
 //   error_log($alias);
    return $this->connections[$alias];
  }

  /**
   *
   * @param class $class
   * @return Connection
   */
  public function getConnectionForModel($class) {
    return $this->connections[$this->modelConnections[$class]];
  }

  /**
   *
   * @param string $alias
   * @param Connection $connection
   */
  public function addConnection($alias, Connection $connection) {
    $this->connections[$alias] = $connection;
  }

  /**
   *
   * @param string $alias
   * @param class $class
   */
  public function bindModelWithConnection($class, $alias) {
    $this->modelConnections[$class] = $alias;
  }

  /**
   * @return mixed
   */
  public function query($queryString, $class, $parameters = null) {
    $connection = $this->getConnectionForModel($class);
    return $connection->query($queryString, $class, $parameters);
  }

  /**
   *
   * @param Model $instance
   */
  public function save($instance) {
    if ($instance->getChanged()) {
      $this->begin();
      $instanceFields = $instance->getFields();
      $class = get_class($instance);

      $fieldsMappingStatic = PicoORM::getStatic($class, 'fieldsMapping');
      $primaryStatic = PicoORM::getStatic($class, 'primary');
      $hasOneStatic = PicoORM::getStatic($class, 'hasOne');
      $hasManyStatic = PicoORM::getStatic($class, 'hasMany');
      $tableNameStatic = PicoORM::getStatic($class, 'tableName');
      $autoIncrementStatic = PicoORM::getStatic($class, 'autoIncrement');

      $connection = $this->getConnectionForModel($class);
      $tablePrimary = $fieldsMappingStatic[$primaryStatic];

      if ($tablePrimary) {
        $primary = $tablePrimary;
      } else {
        $primary = $primaryStatic;
      }

      if (!key_exists($class, $this->statements)) {
        $this->statements[$class] = array();
      }

      if (!key_exists('save'.($instance->isNew() ? 'New' : ''), $this->statements[$class])) {
        $fields = array();

        foreach ($instanceFields as $fieldName => $fieldValue) {
          $rawField = array_search($fieldName, $fieldsMappingStatic);

          if ($rawField) {
            if ($fieldName != $primaryStatic || !$autoIncrementStatic) {
              $fields[$rawField] = $fieldName;
            }
          
          } else {
            if (key_exists($fieldName, $hasOneStatic)) {
              $fieldValue->save();
            } else if (key_exists($fieldName, $hasManyStatic)) {
              foreach ($fieldValue as $object) {
                $object->save();
              }
            } else {
              if ($fieldName != $primaryStatic || !$autoIncrementStatic) {
                $fields[$fieldName] = $fieldName;
              }
            }
          }
        }

        $query = null;
        if ($instance->isNew()) {
          $rawNames = array();
          $aliases =  array();

          foreach ($fields as $fieldName => $fieldAlias) {
            $rawNames[] = $fieldName;
            $aliases[] = ':'.$fieldAlias;
          }

          $query = 'INSERT INTO '. $tableNameStatic;
          $query .= ' ('.  implode(', ', $rawNames).') VALUES ('.implode(', ', $aliases) .')';
        } else {
          $query = 'UPDATE '. $tableNameStatic . ' SET ';
          $arr = array();

          foreach ($fields as $name => $alias) {
            $arr []= "$name = :$alias";
          }

          $where = ' WHERE '. $primaryStatic .' = :'.$primaryStatic.' LIMIT 1';
          $query .= implode(', ', $arr).$where;
        }

        error_log($query);
        $stmt = $connection->getPdo()->prepare($query);

        if (!$stmt) {
          $this->rollback();
          throw new QueryException('Cannot prepare statement: '.json_encode($connection->getPdo()->errorInfo()));
        }

        $this->statements[$class]['save'.($instance->isNew() ? 'New' : '')] = $stmt;
      }

      $stmt = $this->statements[$class]['save'.($instance->isNew() ? 'New' : '')];

      foreach ($instanceFields as $field => $value) {
        if (!$value instanceof Model && !is_array($value) && ($field != $primaryStatic || !$autoIncrementStatic)) {
          $stmt->bindValue(":$field", $value);
        }
      }

      error_log($instanceFields[$primary]);
      if (! $instance->isNew()) {
        $stmt->bindValue(":$primaryStatic", $instanceFields[$primary]);
        foreach ($instance->getChangedObjects() as $object) {
          $object->save();
        }
        $instance->clearChangedObjects();
      }

      if (!$stmt->execute()) {
        $this->rollback();
        throw new QueryException('Cannot execute statement: '.json_encode($stmt->errorInfo()));
      }
      $stmt->closeCursor();
      if ($instance->isNew()) {
        $instance->$primary = $connection->getPdo()->lastInsertId();
        $instance->setNew(false);
        $instance->setChanged(false);
      }
      $this->commit();
    }
  }

  /**
   *
   * @param Model $instance
   */
  public function delete($instance) {
    $this->begin();
    $class = get_class($instance);
    $fieldsMappingStatic = PicoORM::getStatic($class, 'fieldsMapping');
    $primaryStatic = PicoORM::getStatic($class, 'primary');
    $tableNameStatic = PicoORM::getStatic($class, 'tableName');

    $instanceFields = $instance->getFields();

    $tablePrimary = $fieldsMappingStatic[$primaryStatic];

    if ($tablePrimary) {
      $primary = $tablePrimary;
    } else {
      $primary = $primaryStatic;
    }
    if (!key_exists($class, $this->statements)) {
      $this->statements[$class] = array();
    }

    if (!key_exists('delete', $this->statements[$class])) {
      $connection = PicoORM::instance()->getConnectionForModel($class);
      $query = 'DELETE FROM '.$tableNameStatic;

      $where = ' WHERE '. $primaryStatic .' = :'.$primaryStatic. ' LIMIT 1';
      $query .= $where;

      error_log($query);
      $stmt = $connection->getPdo()->prepare($query);
      if (!$stmt) {
        $this->rollback();
        throw new ModelException('Cannot create query: '.$connection->getPdo()->errorInfo());
      }

      $this->statements[$class]['delete'] = $stmt;
    }

    foreach ($instance->getChangedObjects() as $object) {
      $object->save();
    }
    $instance->clearChangedObjects();
    $this->statements[$class]['delete']->bindValue(":$primaryStatic", $instanceFields[$primary]);

    if (!$this->statements[$class]['delete']->execute()) {
      $this->rollback();
      throw new ModelException('Cannot execute query: '.json_encode($this->statements[$class]['delete']->errorInfo()));
    }
    $this->statements[$class]['delete']->closeCursor();
    $this->commit();
  }
}
?>
